import { Buffer } from 'buffer'

function error() {
  let msg = toArray(arguments).join(' ')
  throw new Error(msg)
}
function contains(container, item) {
  if (isString(container)) {
    return container.indexOf(item) !== -1
  } else if (isArrayLike(container)) {
    return indexOf(container, item) !== -1
  }
  error('Expected Array or String argument')
}
function some(arr, test) {
  return arr.reduce((val, item) => {
    return val || test(item) // TODO: short-circuit?
  }, false)
}
function find(arr, test, ctx) {
  const matches = arr.filter(test, ctx)
  return matches.length === 0 ? null : matches[0]
}
function indexOf(arr, item, prop) {
  if (prop) error('utils.indexOf() No longer supports property argument')
  const nan = item !== item
  for (let i = 0, len = arr.length || 0; i < len; i += 1) {
    if (arr[i] === item) return i
    if (nan && arr[i] !== arr[i]) return i
  }
  return -1
}
// Calc sum, skip falsy and NaN values
// Assumes: no other non-numeric objects in array
//
function sum(arr, info) {
  if (!isArrayLike(arr)) error("utils.sum() expects an array, received:", arr)
  let tot = 0
  let nan = 0
  let val = null
  for (let i = 0, n = arr.length; i < n; i += 1) {
    val = arr[i]
    if (val) {
      tot += val
    } else if (isNaN(val)) {
      nan++
    }
  }
  if (info) {
    info.nan = nan
  }
  return tot
}
function uniq(src) {
  const index = {}
  return src.reduce((memo, el) => {
    if (el in index === false) {
      index[el] = true
      memo.push(el)
    }
    return memo
  }, [])
}
function pluck(arr, key) {
  return arr.map((obj) => {
    return obj[key]
  })
}
function countValues(arr) {
  return arr.reduce((memo, val) => {
    memo[val] = (val in memo) ? memo[val] + 1 : 1
    return memo
  }, {})
}
function arrayToIndex(arr, val) {
  const init = arguments.length > 1
  return arr.reduce((index, key) => {
    index[key] = init ? val : true
    return index
  }, {})
}
function forEach(arr, func, ctx) {
  if (!isArrayLike(arr)) {
    throw new Error("#forEach() takes an array-like argument. " + arr)
  }
  for (let i=0, n=arr.length; i < n; i++) {
    func.call(ctx, arr[i], i)
  }
}
function initializeArray(arr, init) {
  for (let i=0, len=arr.length; i<len; i++) {
    arr[i] = init
  }
  return arr
}
function repeatString(src, n) {
  let str = ''
  for (let i = 0; i < n; i += 1) {
    str += src
  }
  return str
}
function lpad(str, size, pad) {
  pad = pad || ' '
  str = String(str)
  return repeatString(pad, size - str.length) + str
}
function trim(str) {
  return ltrim(rtrim(str))
}
const ltrimRxp = /^\s+/
function ltrim(str) {
  return str.replace(ltrimRxp, '')
}
const rtrimRxp = /\s+$/
function rtrim(str) {
  return str.replace(rtrimRxp, '')
}
function regexEscape(str) {
  return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
}
function defaults(dest) {
  for (let i=1, n=arguments.length; i<n; i++) {
    const src = arguments[i] || {}
    for (let key in src) {
      if (key in dest === false && src.hasOwnProperty(key)) {
        dest[key] = src[key]
      }
    }
  }
  return dest
}
function isFunction(obj) {
  return typeof obj == 'function'
}
function isObject(obj) {
  return obj === Object(obj)
}
function isArray(obj) {
  return Array.isArray(obj)
}
function isNumber(obj) {
  return obj != null && obj.constructor == Number
}
function isInteger(obj) {
  return isNumber(obj) && ((obj | 0) === obj)
}
function isBoolean(obj) {
  return obj === true || obj === false
}
function isString(obj) {
  return obj != null && obj.toString === String.prototype.toString
}
function toArray(obj) {
  let arr = null
  if (!isArrayLike(obj)) {
    error("utils.toArray() requires an array-like object")
  }
  try {
    arr = Array.prototype.slice.call(obj, 0)
  } catch (e) {
    arr = []
    for (let i = 0, n = obj.length; i < n; i += 1) {
      arr[i] = obj[i]
    }
  }
  return arr
}
function isArrayLike(obj) {
  if (!obj) return false
  if (isArray(obj)) return true
  if (isString(obj)) return false
  if (obj.length === 0) return true
  if (obj.length > 0) return true
  return false
}
function extend(o) {
  const dest = o || {}
  const n = arguments.length
  for (let i = 1; i < n; i += 1) {
    let src = arguments[i] || {}
    for (let key in src) {
      if (src.hasOwnProperty(key)) {
        dest[key] = src[key]
      }
    }
  }
  return dest
}
function Transform() {
  this.mx = this.my = 1
  this.bx = this.by = 0
}

Transform.prototype.isNull = function() {
  return !this.mx || !this.my || isNaN(this.bx) || isNaN(this.by)
}

Transform.prototype.invert = function() {
  const inv = new Transform()
  inv.mx = 1 / this.mx
  inv.my = 1 / this.my
  inv.bx = -this.bx / this.mx
  inv.by = -this.by / this.my
  return inv
}


Transform.prototype.transform = function(x, y, xy) {
  xy = xy || []
  xy[0] = x * this.mx + this.bx
  xy[1] = y * this.my + this.by
  return xy
}

Transform.prototype.toString = function() {
  return JSON.stringify(utils.extend({}, this))
}
function Bounds() {
  if (arguments.length > 0) {
    this.setBounds.apply(this, arguments)
  }
}
Bounds.prototype.toString = function() {
  return JSON.stringify({
    xmin: this.xmin,
    xmax: this.xmax,
    ymin: this.ymin,
    ymax: this.ymax
  })
}
Bounds.prototype.toArray = function() {
  return this.hasBounds() ? [this.xmin, this.ymin, this.xmax, this.ymax] : []
}
Bounds.prototype.hasBounds = function() {
  return this.xmin <= this.xmax && this.ymin <= this.ymax
}
Bounds.prototype.sameBounds =
Bounds.prototype.equals = function(bb) {
  return bb && this.xmin === bb.xmin && this.xmax === bb.xmax &&
    this.ymin === bb.ymin && this.ymax === bb.ymax
}
Bounds.prototype.width = function() {
  return (this.xmax - this.xmin) || 0
}
Bounds.prototype.height = function() {
  return (this.ymax - this.ymin) || 0
}
Bounds.prototype.area = function() {
  return this.width() * this.height() || 0
}
Bounds.prototype.empty = function() {
  this.xmin = this.ymin = this.xmax = this.ymax = void 0
  return this
}
Bounds.prototype.setBounds = function(a, b, c, d) {
  if (arguments.length == 1) {
    // assume first arg is a Bounds or array
    if (isArrayLike(a)) {
      b = a[1]
      c = a[2]
      d = a[3]
      a = a[0]
    } else {
      b = a.ymin
      c = a.xmax
      d = a.ymax
      a = a.xmin
    }
  }

  this.xmin = a
  this.ymin = b
  this.xmax = c
  this.ymax = d
  if (a > c || b > d) this.update()
  // error("Bounds#setBounds() min/max reversed:", a, b, c, d);
  return this
}
Bounds.prototype.centerX = function() {
  const x = (this.xmin + this.xmax) * 0.5
  return x
}
Bounds.prototype.centerY = function() {
  const y = (this.ymax + this.ymin) * 0.5
  return y
}
Bounds.prototype.containsPoint = function(x, y) {
  if (x >= this.xmin && x <= this.xmax &&
    y <= this.ymax && y >= this.ymin) {
    return true
  }
  return false
}
Bounds.prototype.containsBufferedPoint =
Bounds.prototype.containsCircle = function(x, y, buf) {
  if ( x + buf > this.xmin && x - buf < this.xmax ) {
    if ( y - buf < this.ymax && y + buf > this.ymin ) {
      return true
    }
  }
  return false
}
Bounds.prototype.intersects = function(bb) {
  if (bb.xmin <= this.xmax && bb.xmax >= this.xmin &&
    bb.ymax >= this.ymin && bb.ymin <= this.ymax) {
    return true
  }
  return false
}
Bounds.prototype.contains = function(bb) {
  if (bb.xmin >= this.xmin && bb.ymax <= this.ymax &&
    bb.xmax <= this.xmax && bb.ymin >= this.ymin) {
    return true
  }
  return false
}
Bounds.prototype.shift = function(x, y) {
  this.setBounds(this.xmin + x,
    this.ymin + y, this.xmax + x, this.ymax + y)
}
Bounds.prototype.padBounds = function(a, b, c, d) {
  this.xmin -= a
  this.ymin -= b
  this.xmax += c
  this.ymax += d
}
// Rescale the bounding box by a fraction. TODO: implement focus.
// @param {number} pct Fraction of original extents
// @param {number} pctY Optional amount to scale Y
//
Bounds.prototype.scale = function(pct, pctY) { /*, focusX, focusY*/
  const halfWidth = (this.xmax - this.xmin) * 0.5
  const halfHeight = (this.ymax - this.ymin) * 0.5
  const kx = pct - 1
  const ky = pctY === undefined ? kx : pctY - 1
  this.xmin -= halfWidth * kx
  this.ymin -= halfHeight * ky
  this.xmax += halfWidth * kx
  this.ymax += halfHeight * ky
}
// Return a bounding box with the same extent as this one.
Bounds.prototype.cloneBounds = // alias so child classes can override clone()
Bounds.prototype.clone = function() {
  return new Bounds(this.xmin, this.ymin, this.xmax, this.ymax)
}

Bounds.prototype.clearBounds = function() {
  this.setBounds(new Bounds())
}

Bounds.prototype.mergePoint = function(x, y) {
  if (this.xmin === void 0) {
    this.setBounds(x, y, x, y)
  } else {
    // this works even if x,y are NaN
    if (x < this.xmin)  this.xmin = x
    else if (x > this.xmax)  this.xmax = x

    if (y < this.ymin) this.ymin = y
    else if (y > this.ymax) this.ymax = y
  }
}

// expands either x or y dimension to match @aspect (width/height ratio)
// @focusX, @focusY (optional): expansion focus, as a fraction of width and height
Bounds.prototype.fillOut = function(aspect, focusX, focusY) {
  if (arguments.length < 3) {
    focusX = 0.5
    focusY = 0.5
  }
  const w = this.width()
  const h = this.height()
  const currAspect = w / h
  let pad = null
  if (isNaN(aspect) || aspect <= 0) {
    // error condition; don't pad
  } else if (currAspect < aspect) { // fill out x dimension
    pad = h * aspect - w
    this.xmin -= (1 - focusX) * pad
    this.xmax += focusX * pad
  } else {
    pad = w / aspect - h
    this.ymin -= (1 - focusY) * pad
    this.ymax += focusY * pad
  }
  return this
}
Bounds.prototype.update = function() {
  let tmp = null
  if (this.xmin > this.xmax) {
    tmp = this.xmin
    this.xmin = this.xmax
    this.xmax = tmp
  }
  if (this.ymin > this.ymax) {
    tmp = this.ymin
    this.ymin = this.ymax
    this.ymax = tmp
  }
}
Bounds.prototype.transform = function(t) {
  this.xmin = this.xmin * t.mx + t.bx
  this.xmax = this.xmax * t.mx + t.bx
  this.ymin = this.ymin * t.my + t.by
  this.ymax = this.ymax * t.my + t.by
  this.update()
  return this
}
Bounds.prototype.getTransform = function(b2, flipY) {
  const t = new Transform()
  t.mx = b2.width() / this.width() || 1 // TODO: better handling of 0 w,h
  t.bx = b2.xmin - t.mx * this.xmin
  if (flipY) {
    t.my = -b2.height() / this.height() || 1
    t.by = b2.ymax - t.my * this.ymin
  } else {
    t.my = b2.height() / this.height() || 1
    t.by = b2.ymin - t.my * this.ymin
  }
  return t
}
Bounds.prototype.mergeCircle = function(x, y, r) {
  if (r < 0) r = -r
  this.mergeBounds([x - r, y - r, x + r, y + r])
}
Bounds.prototype.mergeBounds = function(bb) {
  let a, b, c, d
  if (bb instanceof Bounds) {
    a = bb.xmin
    b = bb.ymin
    c = bb.xmax
    d = bb.ymax
  } else if (arguments.length == 4) {
    a = arguments[0]
    b = arguments[1]
    c = arguments[2]
    d = arguments[3]
  } else if (bb.length == 4) {
    // assume array: [xmin, ymin, xmax, ymax]
    a = bb[0]
    b = bb[1]
    c = bb[2]
    d = bb[3]
  } else {
    error("Bounds#mergeBounds() invalid argument:", bb)
  }

  if (this.xmin === void 0) {
    this.setBounds(a, b, c, d)
  } else {
    if (a < this.xmin) this.xmin = a
    if (b < this.ymin) this.ymin = b
    if (c > this.xmax) this.xmax = c
    if (d > this.ymax) this.ymax = d
  }
  return this
}
function findRankByValue(arr, value) {
  if (isNaN(value)) return arr.length
  let rank = 1
  for (let i=0, n=arr.length; i<n; i++) {
    if (value > arr[i]) rank++
  }
  return rank
}
function sortOn(arr) {
  const comparators = []
  for (let i=1; i<arguments.length; i+=2) {
    comparators.push(getKeyComparator(arguments[i], arguments[i+1]))
  }
  arr.sort((a, b) => {
    let cmp = 0
    let i = 0
    const n = comparators.length
    while (i < n && cmp === 0) {
      cmp = comparators[i](a, b)
      i++
    }
    return cmp
  })
  return arr
}
function getKeyComparator(key, asc) {
  const compare = getGenericComparator(asc)
  return function(a, b) {
    return compare(a[key], b[key])
  }
}

function getGenericComparator(asc) {
  asc = asc !== false
  return function(a, b) {
    let retn = 0
    if (b == null) {
      retn = a == null ? 0 : -1
    } else if (a == null) {
      retn = 1
    } else if (a < b) {
      retn = asc ? -1 : 1
    } else if (a > b) {
      retn = asc ? 1 : -1
    } else if (a !== a) {
      retn = 1
    } else if (b !== b) {
      retn = -1
    }
    return retn
  }
}
/**
 * Wrapper for DataView class for more convenient reading and writing of
 * binary data; Remembers endianness and read/write position.
 * Has convenience methods for copying from buffers, etc.
 * @param {*} buf 
 * @param {*} le 
 */
function BinArray(buf, le) {
  if (isNumber(buf)) {
    buf = new ArrayBuffer(buf)
  } else if (typeof Buffer === 'function' && buf instanceof Buffer) {
    buf = BinArray.toArrayBuffer(buf)
  }
  if (buf instanceof ArrayBuffer === false) {
    error(`error("BinArray constructor takes an integer, ArrayBuffer or Buffer argument")`)
  }
  this._buffer = buf
  this._bytes = new Uint8Array(buf)
  this._view = new DataView(buf)
  this._idx = 0
  this._le = le !== false
}
BinArray.bufferToUintArray = function(buf, wordLen) {
  if (wordLen === 4) return new Uint32Array(buf)
  if (wordLen === 2) return new Uint16Array(buf)
  if (wordLen === 1) return new Uint8Array(buf)
  error(`BinArray.bufferToUintArray() invalid word length:${wordLen}`)
}
BinArray.uintSize = function(i) {
  return i & 1 || i & 2 || 4
}
BinArray.bufferCopy = function(dest, destId, src, srcId, bytes) {
  srcId = srcId || 0
  bytes = bytes || src.byteLength - srcId
  if (dest.byteLength - destId < bytes) {
    error(`Buffer overflow; tried to write:${bytes}`)
  }
  const wordSize = Math.min(BinArray.uintSize(bytes), BinArray.uintSize(srcId),
        BinArray.uintSize(dest.byteLength), BinArray.uintSize(destId),
        BinArray.uintSize(src.byteLength))
  const srcArr = BinArray.bufferToUintArray(src, wordSize)
  const destArr = BinArray.bufferToUintArray(dest, wordSize)
  let count = bytes / wordSize
  let i = srcId / wordSize
  let j = destId / wordSize
  while (count--) {
    destArr[j++] = srcArr[i++]
  }
  return bytes
}
BinArray.toArrayBuffer = function(src) {
  const n = src.length
  const dest = new ArrayBuffer(n)
  const view = new Uint8Array(dest)
  for (let i = 0; i < n; i += 1) {
    view[i] = src[i]
  }
  return dest
}
BinArray.bufferSize = function(buf) {
  return (buf instanceof ArrayBuffer ? buf.byteLength : buf.length | 0)
}
BinArray.prototype = {
  size: function() {
    return this._buffer.byteLength
  },
  littleEndian: function() {
    this._le = true
    return this
  },
  bigEndian: function() {
    this._le = false
    return this
  },
  buffer: function() {
    return this._buffer
  },
  bytesLeft: function() {
    return this._buffer.byteLength - this._idx
  },
  skipBytes: function(bytes) {
    this._idx += (bytes + 0)
    return this
  },
  readUint8: function() {
    return this._bytes[this._idx++]
  },
  writeUint8: function(val) {
    this._bytes[this._idx++] = val
    return this
  },
  readInt8: function(val) {
    return this._view.getInt8(this._idx++)
  },
  writeInt8: function(val) {
    this._view.setInt8(this._idx++, val)
    return this
  },
  readUint16: function() {
    const val = this._view.getUint16(this._idx, this._le)
    this._idx += 2
    return val
  },
  writeUint16: function(val) {
    this._view.setUint16(this._idx, val, this._le)
    this._idx += 2
    return this
  },
  readUint32: function() {
    const val = this._view.getUint32(this._idx, this._le)
    this._idx += 4
    return val
  },
  writeUint32: function(val) {
    this._view.setUint32(this._idx, val, this._le)
    this._idx += 4
    return this
  },
  readInt32: function() {
    const val = this._view.getInt32(this._idx, this._le)
    this._idx += 4
    return val
  },
  writeInt32: function(val) {
    this._view.setInt32(this._idx, val, this._le)
    this._idx += 4
    return this
  },
  readFloat64: function() {
    const val = this._view.getFloat64(this._idx, this._le)
    this._idx += 8
    return val
  },
  writeFloat64: function(val) {
    this._view.setFloat64(this._idx, val, this._le)
    this._idx += 8
    return this
  },
  readFloat64Array: function(len) {
    const bytes = len * 8
    const i = this._idx
    const buf = this._buffer
    let arr = null
    if (i % 8 === 0) {
      arr = new Float64Array(buf, i, len)
    } else if (buf.slice) {
      arr = new Float64Array(buf.slice(i, i + bytes))
    } else {
      const dest = new ArrayBuffer(bytes)
      BinArray.bufferCopy(dest, 0, buf, i, bytes)
      arr = new Float64Array(dest)
    }
    this._idx += bytes
    return arr
  },
  readUint32Array: function(len) {
    const arr = []
    for (let i = 0; i < len; i++) {
      arr.push(this.readUint32())
    }
    return arr
  },
  peek: function(i) {
    return this._view.getUint8(i >= 0 ? i : this._idx)
  },
  position: function(i) {
    if (i != null) {
      this._idx = i
      return this
    }
    return this._idx
  },
  readCString: function(fixedLen, asciiOnly) {
    let str = ''
    let count = fixedLen >= 0 ? fixedLen : this.bytesLeft()
    while (count > 0) {
      const byteVal = this.readUint8()
      count --
      if (byteVal === 0) {
        break
      } else if (byteVal > 127 && asciiOnly) {
        str = null
        break
      }
      str += String.fromCharCode(byteVal)
    }
    if (fixedLen > 0 && count > 0) {
      this.skipBytes(count)
    }
    return str
  },
  writeString: function(str, maxLen) {
    let bytesWritten = 0
    let charsToWrite = str.length
    let cval = 0
    if (maxLen) {
      charsToWrite = Math.min(charsToWrite, maxLen)
    }
    for (let i = 0; i < charsToWrite; i += 1) {
      cval = str.charCodeAt(i)
      if (cval > 127) {
        cval = '?'.charCodeAt(0)
      }
      this.writeUint8(cval)
      bytesWritten++
    }
    return bytesWritten
  },
  writeCString: function(str, fixedLen) {
    const maxChars = fixedLen ? fixedLen -1 : null
    let bytesWritten = this.writeString(str, maxChars)
    this.writeUint8(0)
    bytesWritten++
    if (fixedLen) {
      while (bytesWritten < fixedLen) {
        this.writeUint8(0)
        bytesWritten++
      }
    }
    return this
  },
  writeBuffer: function(buf, bytes, startIdx) {
    this._idx += BinArray.bufferCopy(this._buffer, this._idx, buf, startIdx, bytes)
    return this
  }
}
export {
  Bounds,
  BinArray,
}
export default {
  uniq,
  pluck,
  countValues,
  sortOn,
  arrayToIndex,
  forEach,
  initializeArray,
  regexEscape,
  defaults,
  toArray,
  isString,
  isArray,
  isFunction,
  isObject,
  isBoolean,
  extend,
  error,
  contains,
  find,
  trim,
  isNumber,
  isInteger,
  lpad,
  sum,
  findRankByValue,
  some,
}
